package tle.mock;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * Create mock object for interface/class.
 * 
 * <pre>
 * public interface HelloService {
 * 	String hello();
 * 
 * 	String hello(String name);
 * }
 * 
 * HelloService service = MockFactory.create(HelloService.class, new Object() {
 * 	public String hello(String name) {
 * 		return &quot;hi, &quot; + name;
 * 	}
 * });
 * String val = service.hello(&quot;chulsoo&quot;);
 * // val = hi, chulsoo
 * </pre>
 * 
 * Mock object follows the below rule:
 * <ul>
 * <li>If implementation object has a method which matches called method,
 * <ul>
 * <li>then mock call method of implementation object</li>
 * </ul>
 * </li>
 * <li>If not,
 * <ul>
 * <li>call method of target class if called method of target class is not
 * abstract.</li>
 * <li>return default value of return type of called method if target class is
 * interface or called method is abstract.</li>
 * </ul>
 * </li>
 * </ul>
 * 
 * @author Choi Beom Kyun(madvirus)
 */
public class MockFactory {

	/**
	 * Create mock object. Returned object delegate method call to impl if impl
	 * has a method which match called method. Because this class uses cglib,
	 * this does not support final class and method.
	 * 
	 * @param target
	 *            Class instance of interface or class
	 * @param impl
	 *            object which contains replaced method
	 * @return mock
	 */
	public static Object createMock(Class target, Object impl) {
		Callback callback = null;
		try {
			callback = new Callback(target, impl);
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(target);
		enhancer.setCallback(callback);
		Object obj = enhancer.create();
		return obj;
	}

	private static class Callback implements MethodInterceptor {

		private Object object;
		private Class targetClass;

		public Callback(Class targetClass, Object object)
				throws InstantiationException, IllegalAccessException,
				ClassNotFoundException {
			this.targetClass = targetClass;
			boolean anonymousClass = checkAnonymous(object);
			if (anonymousClass) {
				Class klass = loadClass(object);
				this.object = klass.newInstance();
			} else {
				this.object = object;
			}
		}

		private boolean checkAnonymous(Object object) {
			String objectClassName = object.getClass().getName();
			boolean anonymousClass = true;
			boolean localClass = false;
			int subNameIdx = objectClassName.indexOf("$");
			if (subNameIdx > -1) {
				String subName = objectClassName.substring(subNameIdx + 1);
				char[] subNameChs = subName.toCharArray();
				int i = 0;
				for (i = 0; i < subNameChs.length; i++) {
					if (!(subNameChs[i] >= '0' && subNameChs[i] <= '9')) {
						anonymousClass = false;
						break;
					}
				}
				if (!anonymousClass) {
					localClass = i > 0;
				}
			} else {
				anonymousClass = false;
			}
			return anonymousClass || localClass;
		}

		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy proxy) throws Throwable {
			Method[] methods = object.getClass().getMethods();
			Method interceptingMethod = null;
			for (int i = 0; i < methods.length; i++) {
				Method m = methods[i];
				if (checkSignatureEquals(m, method)) {
					interceptingMethod = m;
				}
			}
			if (interceptingMethod == null) {
				if (targetClass.isInterface())
					return null;
				Method targetMethod = null;
				try {
					targetMethod = targetClass.getDeclaredMethod(method
							.getName(), method.getParameterTypes());
				} catch (NoSuchMethodException e) {
				}
				if (targetMethod == null)
					return null;
				return proxy.invokeSuper(obj, args);
			} else {
				try {
					return interceptingMethod.invoke(object, args);
				} catch (InvocationTargetException e) {
					throw e.getTargetException();
				}
			}
		}

		private boolean checkSignatureEquals(Method m1, Method m2) {
			if (!m1.getName().equals(m2.getName()))
				return false;
			if (!m1.getReturnType().equals(m2.getReturnType()))
				return false;
			Class[] m1ParamType = m1.getParameterTypes();
			Class[] m2ParamType = m2.getParameterTypes();
			if (m1ParamType.length != m2ParamType.length)
				return false;
			for (int i = 0; i < m1ParamType.length; i++) {
				if (!m1ParamType[i].equals(m2ParamType[i]))
					return false;
			}
			return true;
		}

		private static Class loadClass(Object obj)
				throws ClassNotFoundException {
			String className = obj.getClass().getName();
			ProxyClassLoader cl3 = new ProxyClassLoader(obj.getClass()
					.getClassLoader(), className, MadeTarget.class.getName());
			return cl3.loadClass(className);
		}
	}
}
